package com.linkoog.devtools.raw_index.core;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.util.indexing.DumbModeAccessType;
import com.linkoog.devtools.raw_index.PsiRawFileIndex;
import com.linkoog.devtools.raw_index.filemap.CachedFileMap;
import com.linkoog.devtools.utils.MD5Util;
import com.linkoog.devtools.utils.VirtualFileUtils;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;

public abstract class PsiMethodRawFileIndex<V extends PsiRawFileIndex.Item<?>> extends UserDataHolderBase implements PsiRawFileIndex<PsiMethod, V>, Disposable {

    private static final Logger log = Logger.getInstance(PsiMethodRawFileIndex.class);
    @Getter
    protected Project project;
    protected File file;
    protected CachedFileMap<String, V> fileMap;
    protected boolean init;

    @Override
    public boolean isInit() {
        return init && fileMap != null;
    }

    @Override
    public void init(@NotNull Project project) throws Exception {
        if (! init){
            this.project = project;
            File file = getCacheFile(project);
            if (! file.exists()){
                final File parentFile = file.getParentFile();
                if (! parentFile.exists()){
                    parentFile.mkdirs();
                }
                if (! file.createNewFile()) throw new IllegalStateException("create file fail: " + file);
            }

            this.file = file;
            Class<V> clazz = getValueClass(project);
            this.fileMap = new CachedFileMap<>(file, String.class, clazz);
            for (String key : fileMap.keySet()) {
                try {
                    V val = fileMap.get(key);
                    if (val == null){
                        fileMap.remove(key);
                        continue;
                    }

                    boolean flag = ApplicationManager.getApplication().runReadAction((Computable<Boolean>) () -> val.initPsiElement(project, null));
                    if (flag) {
                        //初始化成功
                        PsiElement psiElement = val.getPsiElement();
                        savePsiElement(psiElement);
                    } else {
                        fileMap.remove(key);
                    }
                }catch (Exception e){
                    fileMap.remove(key);
                }
            }

            init = true;
        }
    }



    public abstract File getCacheFile(Project project) throws Exception;

    public abstract Class<V> getValueClass(Project project);

    @Override
    public void dispose() {
        if (! init) return;

        try {
            fileMap.close();
        } catch (IOException e) {
        }
    }

    @Override
    public boolean support(PsiElement psiElement){
        if (psiElement instanceof PsiMethod psiMethod){
            return psiMethod.isValid();
        }
        return false;
    }

    public CachedFileMap<String, V> getFileMap() {
        return fileMap;
    }

    @Override
    public void removeCache(@NotNull PsiMethod psiMethod) {
        if (! init) return;
        fileMap.remove(getCacheKey(psiMethod));
        removePsiElement(psiMethod);
    }

    @Override
    public V getCache(@NotNull PsiMethod psiMethod) {
        if (! init) return null;

        VirtualFile virtualFile = getVirtualFile(psiMethod);
        final long timestamp = VirtualFileUtils.getTimestamp(virtualFile);
        final String cacheKey = getCacheKey(psiMethod);
        V val = fileMap.get(cacheKey);

        if ((val == null || !isContainsPsiElement(psiMethod)) || (val != null && timestamp != val.getTimestamp())){
            Optional<V> optional = handle(psiMethod);
            if (optional != null && optional.isPresent()){
                V newVal = optional.get();
                savePsiElement(psiMethod);
                if (newVal != null){
                    val = newVal;
                    fileMap.put(cacheKey, newVal);
                    return newVal;
                } else {
                    fileMap.remove(cacheKey);
                    return null;
                }
            }

            //解析失败
            fileMap.remove(cacheKey);
            removePsiElement(psiMethod);
            return null;
        }
        return val;
    }

    @Override
    public V getCache(@NotNull String key) {
        return fileMap.get(key);
    }

    @Override
    public Collection<V> getCaches() {
        if (! init) return Collections.emptyList();
        return fileMap.values();
    }

    @Override
    public V updateCache(@NotNull PsiMethod psiMethod) {
        if (! init) return null;

        final String cacheKey = getCacheKey(psiMethod);
        Optional<V> optional = handle(psiMethod);
        if (optional != null){
            savePsiElement(psiMethod);
            if (optional.isPresent()) {
                V val = optional.get();
                fileMap.put(cacheKey, val);
                return val;
            }

            fileMap.remove(cacheKey);
            return null;
        }

        //解析失败
        fileMap.remove(cacheKey);
        removePsiElement(psiMethod);
        return null;
    }

    @Override
    public String getCacheKey(@NotNull PsiMethod psiMethod){
        return DumbModeAccessType.RAW_INDEX_DATA_ACCEPTABLE.ignoreDumbMode( () -> {
            Key<String> psiMethodCacheKey = Key.create(psiMethod.getName() + "#" + psiMethod.hashCode());
            String cacheKey = this.getUserData(psiMethodCacheKey);
            if (cacheKey == null){
                VirtualFile virtualFile = psiMethod.getContainingFile().getOriginalFile().getVirtualFile();
                String path = virtualFile.getPath() + "#" + this.getPsiElementSign(psiMethod);
                cacheKey = MD5Util.md5_64(path);
                this.putUserData(psiMethodCacheKey, cacheKey);
            }
            return cacheKey;
        });
    }

    @Override
    public String getPsiElementSign(@NotNull PsiMethod psiMethod){
        return psiMethod.getName() + "#" + psiMethod.getTextOffset();
    }

    @Override
    public void afterFileChange(PsiClass psiClass, boolean forceUpdate){
        final PsiMethod[] psiMethods = psiClass.getMethods();
        for (PsiMethod psiMethod : psiMethods) {
            if (support(psiMethod)){
                if (forceUpdate) {
                    updateCache(psiMethod);
                } else {
                    getCache(psiMethod);
                }
            }
        }
    }

    @Override
    public void afterFileDelete(@NotNull VirtualFile virtualFile) {
        if (! init) return;
        if (virtualFile.isDirectory())  return;

        final String path = virtualFile.getUrl();
        for (String key : fileMap.keySet()) {
            PsiRawFileIndex.Item<?> cache = fileMap.get(key);
            if (path.equals(cache.getFilePath())) {
                fileMap.remove(key);
            }
        }
    }

    private Optional<V> handle(PsiMethod psiMethod) {
        return DumbService.getInstance(project).runReadActionInSmartMode(() -> {
            try {
                return Optional.ofNullable(resolve(psiMethod));
            }catch (ProcessCanceledException ignored){

            } catch (Throwable e) {
                log.error("解析失败 file: " + psiMethod.getContainingFile().getName() + "   method: " + psiMethod.getName(), e);
            }
            return null;
        });
    }





}
